package org.glandais.profiler;

import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectOutputStream;
import java.lang.management.ManagementFactory;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;

import org.glandais.profiler.beans.MethodTrace;
import org.glandais.profiler.beans.ProcessingList;
import org.glandais.profiler.beans.TraceStack;


public class TraceRecorder {

	private static final ArrayList<TraceStack> stacks = new ArrayList<TraceStack>();
	private static final SimpleDateFormat SIMPLE_DATE_FORMAT_FILE = new SimpleDateFormat(
			"HH-mm-ss.SSS");

	private static synchronized void addTraceStack(TraceStack traceStack) {
		stacks.add(traceStack);
		if (Agent.configBean.isLogVerbose()) {
			Agent.configBean.logVerbose("Stacks count : " + stacks.size());
		}
	}

	private static final ThreadLocal<TraceStack> callStackTL = new ThreadLocal<TraceStack>() {
		@Override
		protected TraceStack initialValue() {
			return new TraceStack();
		}
	};
	private static ThreadLocal<Boolean> logging = new ThreadLocal<Boolean>() {
		@Override
		protected Boolean initialValue() {
			return Boolean.TRUE;
		}
	};

	public static final ThreadLocal<ProcessingList> totalTimeTL = new ThreadLocal<ProcessingList>() {
		@Override
		protected ProcessingList initialValue() {
			return new ProcessingList();
		}
	};

	private static final String hashCode(Object called) {
		if (called != null) {
			return Integer.toHexString(System.identityHashCode(called));
		} else {
			return "?";
		}
	}

	public static final void enter(String type, Object called, String signature) {
		totalTimeTL.get().startProcessing();
		enterRaw(type, called, signature);
		totalTimeTL.get().stopProcessing();
	}

	public static final void exit(String type, Object called, String signature) {
		totalTimeTL.get().startProcessing();
		exitRaw(type, called, signature);
		totalTimeTL.get().stopProcessing();
	}

	private static final void enterRaw(String type, Object called,
			String signature) {
		if (logging.get()) {
			logging.set(Boolean.FALSE);
			try {
				TraceStack traceStack = callStackTL.get();
				if (Agent.configBean.isLogTraceMethodLogs()) {
					Agent.configBean
							.logTraceMethodLogs("Entering " + signature);
				}
				boolean doTrace = true;
				MethodTrace traceMethod = null;
				if (traceStack.stack.empty()) {
					traceStack.stackStart = System.currentTimeMillis();
					traceStack.processingNanos = totalTimeTL.get();
					if (Agent.configBean.getEntryMethods().size() > 0) {
						doTrace = false;
						for (String entryMethod : Agent.configBean
								.getEntryMethods()) {
							if (!doTrace && signature.contains(entryMethod)) {
								if (Agent.configBean.isLogTraceMethodLogs()) {
									Agent.configBean
											.logTraceMethodLogs("Entry method "
													+ signature);
								}
								doTrace = true;
							}
						}
					}
				}
				if (doTrace) {
					traceMethod = new MethodTrace();
					if (!traceStack.stack.empty()) {
						traceStack.stack.peek().subMethods.add(traceMethod);
					} else {
						traceStack.entryMethod = traceMethod;
					}
					traceStack.stack.push(traceMethod);

					traceMethod.called = hashCode(called);
					traceMethod.signature = signature;
					traceMethod.type = type;
					traceMethod.startSystemNanoTime = System.nanoTime();
				}
			} catch (Throwable e) {
				if (Agent.configBean.isLogError()) {
					Agent.configBean.logError("enterRaw", e);
				}
			} finally {
				logging.set(Boolean.TRUE);
			}
		}
	}

	private static final void exitRaw(String type, Object called,
			String signature) {
		if (logging.get()) {
			logging.set(Boolean.FALSE);
			try {
				TraceStack traceStack = callStackTL.get();
				if (Agent.configBean.isLogTraceMethodLogs()) {
					Agent.configBean.logTraceMethodLogs("Exiting " + signature);
				}
				if (!traceStack.stack.empty()) {
					MethodTrace traceMethod = traceStack.stack.pop();
					traceMethod.durationNano = System.nanoTime()
							- traceMethod.startSystemNanoTime;
					if (traceStack.stack.isEmpty()) {
						callStackTL.set(new TraceStack());
						if (traceMethod.getDurationMillis() >= Agent.configBean
								.getMinLengthMsGlobal()) {
							if (Agent.configBean.isLogTraceMethodLogs()) {
								Agent.configBean
										.logTraceMethodLogs("Stack finished "
												+ traceMethod
														.getDurationMillis()
												+ " " + signature);
							}
							addTraceStack(traceStack);
							if (Agent.configBean.isSaveSnapshots()) {
								saveStack(traceStack);
							}
						}
					} else {
						if (traceMethod.getDurationMillis() < Agent.configBean
								.getMinLengthMsMethod()) {
							if (Agent.configBean.isLogTraceMethodLogs()) {
								Agent.configBean
										.logTraceMethodLogs("Too short "
												+ traceMethod
														.getDurationMillis()
												+ " " + signature);
							}
							traceStack.stack.peek().subMethods
									.remove(traceMethod);
						}
					}
				}
			} catch (Throwable e) {
				if (Agent.configBean.isLogError()) {
					Agent.configBean.logError("exitRaw", e);
				}
			} finally {
				logging.set(Boolean.TRUE);
			}
		}
	}

	// private static final String getStringDate(Date date) {
	// return SIMPLE_DATE_FORMAT_FILE.format(date);
	// }

	private static final String getStringDate(long date) {
		return SIMPLE_DATE_FORMAT_FILE.format(new Date(date));
	}

	private static final void saveStack(TraceStack traceStack) {
		String startTime = getStringDate(ManagementFactory.getRuntimeMXBean()
				.getStartTime());

		File subFolder = new File(Agent.configBean.getDirectory(), startTime);
		String startTimeMeth = getStringDate(traceStack.stackStart);
		String l = Long.toString(traceStack.entryMethod.durationNano);
		String fileName = startTimeMeth + "-" + l + ".xml";

		if (Agent.configBean.isLogVerbose()) {
			Agent.configBean.logVerbose("Save stack to " + fileName + " : "
					+ traceStack);
		}

		File newFile = new File(subFolder, fileName);
		try {
			saveObject(traceStack, newFile);
		} catch (Exception e) {
			System.err.println("Echec à la sauvegarde du profiling");
			e.printStackTrace(System.err);
		}
	}

	@SuppressWarnings("unchecked")
	public static final void shutdown() {
		logging = new ThreadLocal<Boolean>() {
			@Override
			protected Boolean initialValue() {
				return Boolean.FALSE;
			}
		};
		ClassFileTransformer.doTransformClasses = false;

		ArrayList<TraceStack> stacksCopy = (ArrayList<TraceStack>) stacks
				.clone();
		String startTime = getStringDate(ManagementFactory.getRuntimeMXBean()
				.getStartTime());

		String fileName = startTime + ".orig.xml";
		if (Agent.configBean.isLogVerbose()) {
			Agent.configBean.logVerbose("Save stacks to " + fileName);
			Agent.configBean.logVerbose("Stacks count : " + stacksCopy.size());
		}

		File f = new File(new File(Agent.configBean.getDirectory()), fileName);
		try {
			saveObject(stacksCopy, f);
		} catch (IOException e) {
			throw new RuntimeException(e);
		}

		String fileNameProcessed = startTime + ".xml";
		if (Agent.configBean.isLogVerbose()) {
			Agent.configBean.logVerbose("Save processed stacks to "
					+ fileNameProcessed);
			Agent.configBean.logVerbose("Stacks count : " + stacksCopy.size());
		}
		for (TraceStack traceStack : stacksCopy) {
			traceStack.removeProcessing();
		}

		File fProcessed = new File(new File(Agent.configBean.getDirectory()),
				fileNameProcessed);
		try {
			saveObject(stacksCopy, fProcessed);
		} catch (IOException e) {
			throw new RuntimeException(e);
		}

	}

	private static final void saveObject(Object o, File f)
			throws FileNotFoundException, IOException {
		if (!f.getParentFile().exists()) {
			f.getParentFile().mkdirs();
		}

		FileOutputStream fos = new FileOutputStream(f);
		ObjectOutputStream xOOS = Agent.xstream.createObjectOutputStream(fos);
		xOOS.writeObject(o);
		xOOS.close();
	}

}
